CPython GIL 的全称是 global interpreter lock。
GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。
http://cenalulu.github.io/python/gil-in-python/
https://www.zhihu.com/question/23030421
https://blog.csdn.net/AnyThingFromBigban/article/details/73611386
为什么会有GIL
为了更有效的利用多核处理器的性能,就出现了多线程的编程方式,而随之带来的就是线程间数据一致性和状态同步的困难。
为了利用多核,Python开始支持多线程。而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 这把超级大锁就是GIL。
假设在一台多核电脑上,两个线程同时执行,同时对一个共享变量赋值,那么应该按照谁的来?这样必然导致数据出错。因此python解释器使用GIL来约束同一时刻只能有一个线程在执行。
为什么有了GIL还需要线程同步
但使用GIL只是解决数据冲突的第一步。
在两个线程不同时执行的前提下,数据冲突的状况还是可能发生。
python执行的过程是:.py文件 -> Python ByteCode -> Python Interpreter (VM contained) exectues ByteCode.
对于数值加一操作,这个操作需要多个bytecodes操作。
1 | lambda x: x+1) dis.dis( |
在执行这个操作的多条bytecodes期间的时候可能中途就换thread了,这样就出现了data races的情况了。
举个例子:
线程AB同时操作list
list的[0]初始值为0
线程A 操作100次
list[0]+=1
线程B 操作100次
list[0]+=1
在线程A 对于 list[0]进行操作时
list[0]为0, 还没等线程A完成加一操作, 就被切换到线程B了
在线程B 眼里,
list[0]还是为0, 于是执行加一操作.
再切换回线程A, 继续未完成的加一操作
线程AB各对list[0]进行了加一,预期结果是2 但结果还是1
GIL的影响
锁的竞争极大影响了多核多线程上程序的效率1
2
3
4
5
6
7
8
9
10
11
12
13
14//Python 3.2之前的GIL机制,伪代码
while(true)
{
execute_statement(); // 执行python代码
if(--py_ticker < 0)
{
py_ticker = check_interval;
sig(gil_cond); // 释放控制权
wait(gil_cond, INFINITE); // 获得控制权
}
}
/*
每次线程释放控制权后,其立马进入等待状态。而在多核情况下,往往由于拥有者比在其它CPU运行的线程醒的更早——也就是自己释放后,立马自己又获得了。而其它CPU上运行的线程被唤醒时,发现锁还是被别人占着,空欢喜一场,无奈只能继续休眠等待。
*/
Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少有一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
Python 3.2中新的GIL机制
新机制中,明确区划分了2个角色owner和waiter。owner根据waiter发来的请求,来释放控制权;采用了时间片,而不是老机制中100多个字节码;owner通知waiter后,它会等待waiter们中的一个真正获得控制权,然后自己才会从owner转变为waiter
实现细节,可以参见python源码 Python-3.2.2/Python/ceval_gil.c 中的take_gil()、drop_gil()这两个函数。
1 | //owner |
1 | // waiter |
如何避免受到GIL的影响
用multiprocessing替代Thread(性能和开发成本的权衡)
multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。
当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。
用其他解析器(功能和性能的权衡)
像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。
总结
Python GIL其实是功能和性能之间权衡后的产物,它尤其存在的合理性,也有较难改变的客观因素。从本分的分析中,我们可以做以下一些简单的总结:
- 因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能
- 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现
- GIL在较长一段时间内将会继续存在,但是会不断对其进行改进